package org.apache.ibatis.executor.keygen;

import org.apache.ibatis.binding.BindingException;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.ExecutorException;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.TypeHandler;
import org.apache.ibatis.type.TypeHandlerRegistry;

import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.*;

/**
 * @author Clinton Begin
 * @author Kazuki Shimizu
 */
public class Jdbc3KeyGenerator implements KeyGenerator {

    /**
     * A shared instance.
     *
     * @since 3.4.3
     */
    public static final Jdbc3KeyGenerator INSTANCE = new Jdbc3KeyGenerator();

    @Override
    public void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
        // do nothing
    }

    @Override
    public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
        // 将用户传入的实参parameter封装成集合类型，然后传入processBatch()方法中处理
        processBatch(ms, stmt, getParameters(parameter));
    }

    /**
     * 1.获取主键数组(keyProperties)
     * 2.获取 ResultSet 元数据
     * 3.遍历参数列表，为每个主键属性获取 TypeHandler
     * 4.从 ResultSet 中获取主键数据，并填充到参数中
     */
    public void processBatch(MappedStatement ms, Statement stmt, Collection<Object> parameters) {
        ResultSet rs = null;
        try {
            // 获取数据库自动生成的主键，如果没有生成主键，则返回结果集为空
            rs = stmt.getGeneratedKeys();
            final Configuration configuration = ms.getConfiguration();
            final TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
            // 获取主键字段
            // 获得keyProperties属性指定的属性名称，它表示主键对应的属性名称
            final String[] keyProperties = ms.getKeyProperties();
            // 获取结果集 ResultSet 的元数据
            // 获取ResultSet的元数据信息
            final ResultSetMetaData rsmd = rs.getMetaData();
            TypeHandler<?>[] typeHandlers = null;
            // 检测数据库生成的主键的列数与keyProperties属性指定的列数是否匹配
            // ResultSet 中数据的列数要大于等于主键的数量
            if (keyProperties != null && rsmd.getColumnCount() >= keyProperties.length) {
                // 遍历 parameters
                for (Object parameter : parameters) {
                    // parameters中有多少元素，就对应生成多少个主键
                    // there should be one row for each statement (also one for each parameter)
                    // 对于批量插入，ResultSet 会返回多行数据
                    if (!rs.next()) {
                        break;
                    }
                    final MetaObject metaParam = configuration.newMetaObject(parameter);
                    if (typeHandlers == null) {
                        // 为每个主键属性获取 TypeHandler
                        typeHandlers = getTypeHandlers(typeHandlerRegistry, metaParam, keyProperties, rsmd);
                    }
                    // 填充结果到运行时参数中
                    // 将生成的主键设置到用户传入的参数的对应位置
                    populateKeys(rs, metaParam, keyProperties, typeHandlers);
                }
            }
        } catch (Exception e) {
            throw new ExecutorException("Error getting generated key or setting result to parameter object. Cause: " + e, e);
        } finally {
            // 关闭rs结果集
            if (rs != null) {
                try {
                    rs.close();
                } catch (Exception e) {
                    // ignore
                }
            }
        }
    }

    private Collection<Object> getParameters(Object parameter) {
        Collection<Object> parameters = null;
        if (parameter instanceof Collection) {
            parameters = (Collection) parameter;
        } else if (parameter instanceof Map) {
            Map parameterMap = (Map) parameter;
            /*
             * 如果 parameter 是 Map 类型，则从其中提取指定 key 对应的值。
             * 至于 Map 中为什么会出现 collection/list/array 等键。大家
             * 可以参考 DefaultSqlSession 的 wrapCollection 方法
             */
            if (parameterMap.containsKey("collection")) {
                parameters = (Collection) parameterMap.get("collection");
            } else if (parameterMap.containsKey("list")) {
                parameters = (List) parameterMap.get("list");
            } else if (parameterMap.containsKey("array")) {
                parameters = Arrays.asList((Object[]) parameterMap.get("array"));
            }
        }
        if (parameters == null) {
            parameters = new ArrayList<Object>();
            parameters.add(parameter);
        }
        return parameters;
    }

    private TypeHandler<?>[] getTypeHandlers(TypeHandlerRegistry typeHandlerRegistry, MetaObject metaParam, String[] keyProperties, ResultSetMetaData rsmd) throws SQLException {
        TypeHandler<?>[] typeHandlers = new TypeHandler<?>[keyProperties.length];
        for (int i = 0; i < keyProperties.length; i++) {
            if (metaParam.hasSetter(keyProperties[i])) {
                TypeHandler<?> th;
                try {
                    Class<?> keyPropertyType = metaParam.getSetterType(keyProperties[i]);
                    th = typeHandlerRegistry.getTypeHandler(keyPropertyType, JdbcType.forCode(rsmd.getColumnType(i + 1)));
                } catch (BindingException e) {
                    th = null;
                }
                typeHandlers[i] = th;
            }
        }
        return typeHandlers;
    }

    /**
     * populateKeys 方法首先是遍历主键数组，
     * 然后通过 TypeHandler 从 ResultSet 中获取自增主键的值，
     * 最后再通过元信息对象将自增主键的值设置到参数中。
     */
    private void populateKeys(ResultSet rs, MetaObject metaParam, String[] keyProperties, TypeHandler<?>[] typeHandlers) throws SQLException {
        // 遍历 keyProperties
        for (int i = 0; i < keyProperties.length; i++) {
            // 获取主键属性
            String property = keyProperties[i];
            TypeHandler<?> th = typeHandlers[i];
            if (th != null) {
                // 从 ResultSet 中获取某列的值
                Object value = th.getResult(rs, i + 1);
                // 设置结果值到运行时参数中
                metaParam.setValue(property, value);
            }
        }
    }

}
